﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Spanner
{
    /// <summary>
    /// This class represents HTML elements.  Typically an element will have a tag ("p", "div", "li", etc.),
    /// zero or more attributes (e.g., "id='xyz'"), and zero or more children (i.e., a list of sub-elements).
    /// Note there are exceptions, for example <see cref="HtmlComment"/> which denotes HTML comments.
    /// </summary>
    public class Html
    {
        internal string Tag;
        internal Attrs Attrs;
        internal Html[] Children;

        public static implicit operator Html(string x) { return new PlaintextHtml { Tag = "plaintexthtml", Text = x }; }

        /// <summary>
        /// Add an attribute to an HTML element.
        /// </summary>
        /// <param name="key">The attribute name.</param>
        /// <param name="value">An arbitrary value which will be represented in JSON format.</param>
        /// <returns>An augmented copy of the original HTML element, which is left unchanged.</returns>
        public virtual Html WithAttr(string key, object value)
        {
            return new Html
            {
                Tag = Tag,
                Attrs = (Attrs ?? new Attrs()).WithAttr(key, new SimpleAttrExpr { Value = value }),
                Children = Children
            };
        }

        /// <summary>
        /// Add a positive Boolean attribute to an HTML element.
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public virtual Html WithAttr(string key)
        {
            return WithAttr(key, true);
        }

        public virtual Html WithClass(params string[] classNames)
        {
            return (classNames == null) ? this : WithAttr("class", string.Join(" ", classNames));
        }

        /// <summary>
        /// Using HtmlIDs helps guarantee the integrity of the relationship between
        /// IDs in HTML and IDs in the generated JavaScript.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public virtual Html WithID(HtmlID id)
        {
            return WithAttr("id", id.Name);
        }

        /// <summary>
        /// Add children to an HTML element (these children are added after any existing children).
        /// </summary>
        /// <param name="children"></param>
        /// <returns>An augmented copy of the original HTML element, which is left unchanged.</returns>
        public virtual Html WithChildren(params Html[] children)
        {
            // Include any existing children first.
            children =
                ((this.Children == null || this.Children.Length == 0)
                ? children
                : this.Children.Concat(children).ToArray()
                );

            return new Html
            {
                Tag = Tag,
                Attrs = Attrs,
                Children = children
            };
        }

        public Html WithChildren(IEnumerable<Html> children)
        {
            return WithChildren(children.ToArray());
        }

        /// <summary>
        /// Add (or extend) a Knockout data-bind attribute to an HTML element.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key">The name of the Knockout binding.</param>
        /// <param name="value">A value which will be represented in JSON format.</param>
        /// <returns>An augmented copy of the original HTML element, which is left unchanged.</returns>
        public virtual Html DataBind<T>(string key, Expr<T> value)
        {
            return new Html
            {
                Tag = Tag,
                Attrs = (Attrs ?? new Attrs()).WithAttr("data-bind", new DataBindExpr { DataBindKey = key, DataBindValue = value }),
                Children = Children
            };
        }

        internal virtual Html DataBind(string key, ITerm term)
        {
            return new Html
            {
                Tag = Tag,
                Attrs = (Attrs ?? new Attrs()).WithAttr("data-bind", new DataBindExpr { DataBindKey = key, DataBindValue = term }),
                Children = Children
            };
        }

        internal virtual void WriteHtml(Context ctxt) {
            WriteStartTag(ctxt);
            WriteChildren(ctxt);
            WriteEndTag(ctxt);
        }

        internal virtual void WriteStartTag(Context ctxt)
        {
            if (InlineTags.Contains(Tag))
            {
                ctxt.Write("<{0}", Tag);
                if (Attrs != null) Attrs.WriteHtml(ctxt);
                ctxt.Write(">");
            }
            else
            {
                ctxt.WriteIndented("<{0}", Tag);
                if (Attrs != null) Attrs.WriteHtml(ctxt);
                ctxt.WriteLine(">");
            }
        }

        internal virtual void WriteChildren(Context ctxt)
        {
            if (Children == null) return;
            ctxt.Indentation++;
            foreach (var child in Children) child.WriteHtml(ctxt);
            ctxt.Indentation--;
        }

        internal virtual void WriteEndTag(Context ctxt)
        {
            if (NoCloseTags.Contains(Tag)) return;
            if (InlineTags.Contains(Tag))
                ctxt.Write("</{0}>", Tag);
            else
                ctxt.WriteIndentedLine("</{0}>", Tag);
        }

        /// <summary>
        /// These tags are never closed in HTML5.
        /// </summary>
        internal static readonly HashSet<string> NoCloseTags = new HashSet<string>
        {
            "area", "base", "br", "col", "command", "embed", "hr", "img",
            "input", "keygen", "link", "meta", "param", "source", "track",
            "wbr"
        };

        internal static readonly HashSet<string> InlineTags = new HashSet<string>
        {
            "a", "label", "abbr", "legend", "address", "link", "area",
            "mark", "audio", "meter", "bm", "text", "nav", "cite",
            "optgroup", "code", "option", "del", "q", "details", "small",
            "dfn", "select", "command", "source", "datalist", "span",
            "em", "strong", "font", "sub", "i", "summary", "iframe",
            "sup", "img", "tbody", "input", "td", "ins", "time", "kbd",
            "var",
            "plaintexthtml" // This is a Spanner pseudo-element.
        };
    }

    internal class PlaintextHtml : Html
    {
        internal string Text;

        internal override void WriteHtml(Context ctxt)
        {
            ctxt.WritePlain(Text);
        }

        public override Html WithAttr(string key, object value)
        {
            return UI.Span(this).WithAttr(key, value);
        }

        public override Html DataBind<T>(string key, Expr<T> value)
        {
            return UI.Span(this).DataBind(key, value);
        }
    }

}
